import { faker } from '@faker-js/faker';
import { text } from 'vest-utils';
import { IsolateSerializer } from 'vestjs-runtime';
import { describe, it, expect, vi } from 'vitest';

import { ErrorStrings } from '../../../errors/ErrorStrings';
import { TFieldName } from '../../../suiteResult/SuiteResultTypes';
import { TestPromise } from '../../../testUtils/testPromise';
import { enforce } from '../../../vest';
import * as vest from '../../../vest';
import { TIsolateTest } from '../../isolate/IsolateTest/IsolateTest';
import { VestTest } from '../../isolate/IsolateTest/VestTest';

let testObject: TIsolateTest;

describe("Test Vest's `test` function", () => {
  describe('test callbacks', () => {
    describe('Warn hook', () => {
      it('Should be marked as warning when the warn hook gets called', () => {
        vest
          .create(() => {
            testObject = vest.test(
              faker.lorem.word() as TFieldName,
              faker.lorem.sentence(),
              () => {
                vest.warn();
              },
            );
          })
          .run();
        expect(VestTest.warns(testObject).unwrap()).toBe(true);
      });
    });

    describe('Sync', () => {
      it('Should be marked as failed after a thrown error', () => {
        vest
          .create(() => {
            testObject = vest.test(
              faker.lorem.word() as TFieldName,
              faker.lorem.sentence(),
              () => {
                throw new Error();
              },
            );
          })
          .run();
        expect(VestTest.isFailing(testObject).unwrap()).toBe(true);
      });

      it('Should be marked as failed for an explicit false return', () => {
        vest
          .create(() => {
            vest.test(
              faker.lorem.word() as TFieldName,
              faker.lorem.sentence(),
              () => false,
            );
          })
          .run();
        expect(VestTest.isFailing(testObject).unwrap()).toBe(true);
      });

      describe('Thrown with a message', () => {
        describe('When field has a message', () => {
          it("Should use field's own message", () => {
            const res = vest
              .create(() => {
                vest.test(
                  'field_with_message' as TFieldName,
                  'some_field_message',
                  () => {
                    failWithString();
                  },
                );
                vest.test(
                  'warning_field_with_message' as TFieldName,
                  'some_field_message',
                  () => {
                    vest.warn();
                    failWithString();
                  },
                );
              })
              .run();

            expect(res.getErrors('field_with_message' as TFieldName)).toEqual([
              'some_field_message',
            ]);
            expect(
              res.tests['field_with_message' as TFieldName].errors,
            ).toEqual(['some_field_message']);
            expect(
              res.getWarnings('warning_field_with_message' as TFieldName),
            ).toEqual(['some_field_message']);
            expect(
              res.tests['warning_field_with_message' as TFieldName].warnings,
            ).toEqual(['some_field_message']);
          });
        });
        describe('When field does not have a message', () => {
          it('Should use message from enforce().message()', () => {
            const res = vest
              .create(() => {
                vest.test('field_without_message' as TFieldName, () => {
                  enforce(100).message('some_field_message').equals(0);
                });
              })
              .run();

            expect(
              res.getErrors('field_without_message' as TFieldName),
            ).toEqual(['some_field_message']);
          });
          it('Should use message from thrown error', () => {
            const res = vest
              .create(() => {
                vest.test('field_without_message' as TFieldName, () => {
                  failWithString();
                });
                vest.test('warning_field_without_message' as TFieldName, () => {
                  vest.warn();
                  failWithString();
                });
              })
              .run();

            expect(
              res.getErrors('field_without_message' as TFieldName),
            ).toEqual(['I fail with a message']);
            expect(
              res.tests['field_without_message' as TFieldName].errors,
            ).toEqual(['I fail with a message']);
            expect(
              res.getWarnings('warning_field_without_message' as TFieldName),
            ).toEqual(['I fail with a message']);
            expect(
              res.tests['warning_field_without_message' as TFieldName].warnings,
            ).toEqual(['I fail with a message']);
          });
        });
      });
    });

    describe('async', () => {
      it('Should be marked as failed when a returned promise rejects', () =>
        TestPromise(done => {
          vest
            .create(() => {
              testObject = vest.test(
                faker.lorem.word() as TFieldName,
                faker.lorem.sentence(),
                () =>
                  new Promise((_, reject) => {
                    expect(VestTest.isFailing(testObject).unwrap()).toBe(false);
                    setTimeout(reject, 300);
                  }),
              );
              expect(VestTest.isFailing(testObject).unwrap()).toBe(false);
              setTimeout(() => {
                expect(VestTest.isFailing(testObject).unwrap()).toBe(true);
                done();
              }, 310);
            })
            .run();
        }));
    });
  });

  describe('test params', () => {
    let testObject: TIsolateTest;
    it('creates a test without a message and without a key', () => {
      vest
        .create(() => {
          testObject = vest.test('field_name' as TFieldName, () => undefined);
        })
        .run();
      expect(testObject.data.fieldName).toBe('field_name');
      expect(testObject.key).toBeNull();
      expect(testObject.data.message).toBeUndefined();
      expect(testObject).toMatchSnapshot();
    });

    it('creates a test without a key', () => {
      vest
        .create(() => {
          testObject = vest.test(
            'field_name' as TFieldName,
            'failure message',
            () => undefined,
          );
        })
        .run();
      expect(testObject.data.fieldName).toBe('field_name');
      expect(testObject.key).toBeNull();
      expect(testObject.data.message).toBe('failure message');
      expect(testObject).toMatchSnapshot();
    });

    it('creates a test without a message and with a key', () => {
      vest
        .create(() => {
          testObject = vest.test(
            'field_name' as TFieldName,
            () => undefined,
            'keyboardcat',
          );
        })
        .run();
      expect(testObject.data.fieldName).toBe('field_name');
      expect(testObject.key).toBe('keyboardcat');
      expect(testObject.data.message).toBeUndefined();
      expect(testObject).toMatchSnapshot();
    });

    it('creates a test with a message and with a key', () => {
      vest
        .create(() => {
          testObject = vest.test(
            'field_name' as TFieldName,
            'failure message',
            () => undefined,
            'keyboardcat',
          );
        })
        .run();
      expect(testObject.data.fieldName).toBe('field_name');
      expect(testObject.key).toBe('keyboardcat');
      expect(testObject.data.message).toBe('failure message');
      expect(IsolateSerializer.serialize(testObject, v => v)).toMatchSnapshot();
    });

    it('throws when field name is not a string', () => {
      const control = vi.fn();
      vest
        .create(() => {
          // @ts-ignore
          expect(() => vest.test(undefined, () => undefined)).toThrow(
            text(ErrorStrings.INVALID_PARAM_PASSED_TO_FUNCTION, {
              fn_name: 'test',
              param: 'fieldName',
              expected: 'string',
            }),
          );
          expect(() =>
            // @ts-expect-error
            vest.test(null, 'error message', () => undefined),
          ).toThrow(
            text(ErrorStrings.INVALID_PARAM_PASSED_TO_FUNCTION, {
              fn_name: 'test',
              param: 'fieldName',
              expected: 'string',
            }),
          );
          expect(() =>
            // @ts-expect-error
            vest.test(null, 'error message', () => undefined, 'key'),
          ).toThrow(
            text(ErrorStrings.INVALID_PARAM_PASSED_TO_FUNCTION, {
              fn_name: 'test',
              param: 'fieldName',
              expected: 'string',
            }),
          );
          control();
        })
        .run();
      expect(control).toHaveBeenCalled();
    });

    it('throws when callback is not a function', () => {
      const control = vi.fn();
      vest
        .create(() => {
          // @ts-expect-error
          expect(() => vest.test('x')).toThrow(
            text(ErrorStrings.INVALID_PARAM_PASSED_TO_FUNCTION, {
              fn_name: 'test',
              param: 'callback',
              expected: 'function',
            }),
          );
          // @ts-expect-error
          expect(() => vest.test('x', 'msg', undefined)).toThrow(
            text(ErrorStrings.INVALID_PARAM_PASSED_TO_FUNCTION, {
              fn_name: 'test',
              param: 'callback',
              expected: 'function',
            }),
          );
          // @ts-expect-error
          expect(() => vest.test('x', 'msg', undefined, 'key')).toThrow(
            text(ErrorStrings.INVALID_PARAM_PASSED_TO_FUNCTION, {
              fn_name: 'test',
              param: 'callback',
              expected: 'function',
            }),
          );
          control();
        })
        .run();
      expect(control).toHaveBeenCalled();
    });
  });
});

function failWithString(msg?: string) {
  throw msg ?? 'I fail with a message';
}
